﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Web;
using System.Web.Caching;
using System.Web.DynamicData;
using System.Web.Security;
using System.Web.Util;
using System.Xml.Linq;
using NotAClue;
using NotAClue.ComponentModel.DataAnnotations;

namespace NotAClue.Web.DynamicData
{
    //http://msdn.microsoft.com/en-us/library/Aa479033
    //http://forums.asp.net/p/1022105/1384993.aspx
    //http://msdn.microsoft.com/en-us/magazine/cc163657.aspx
    public class SecureMetadataSiteMapProvider : SiteMapProvider
    {
        #region Constants
        private const String PROVIDER_NAME = "SecureMetadataSiteMapProvider";
        private const string CACHE_DEPENDENCY_NAME = "SecureMetaDataSiteMapCacheDependency";
        private const String DESCRIPTION = "description";
        private const String SITE_MAP_FILE = "SiteMapFile";
        private const String DEFAULT_SITE_MAP_FILE = "Web.sitemap";
        private const string ANONYMOUS = "anonymous";
        private const String ALLOW_ANONYMOUS_ACCESS = "AllowAnonymousAccess";
        #endregion

        #region Fields
        private readonly object _lock = new object();
        private Dictionary<String, SiteMapNode> _rootNode = new Dictionary<String, SiteMapNode>();
        private Dictionary<String, Hashtable> _urlTable = new Dictionary<String, Hashtable>();
        private Dictionary<String, Hashtable> _keyTable = new Dictionary<String, Hashtable>();
        private Dictionary<String, Hashtable> _childNodeCollectionTable = new Dictionary<string, Hashtable>();
        private Dictionary<String, Hashtable> _parentNodeTable = new Dictionary<string, Hashtable>();
        private String _siteMapFile;
        private String _username;
        private Boolean _allowAnonymousAccess = false;
        private MetaModel _model;
        #endregion

        #region Properties
        public MetaModel Model
        {
            get
            {
                if (_model == null)
                    _model = MetaModel.Default;
                return this._model;
            }
            set { this._model = value; }
        }

        public override SiteMapNode RootNode
        {
            get
            {
                // make sure the site map is built
                this.BuildSiteMap();

                if (IsValidUser && _rootNode.Keys.Contains(_username) && _rootNode[_username] != null)
                    return _rootNode[_username];

                return null;
            }
        }

        private Boolean IsValidUser
        {
            get
            {
                if (_allowAnonymousAccess)
                {
                    _username = ANONYMOUS;
                    return true;
                }
                else if (HttpContext.Current.User.Identity.IsAuthenticated)
                {
                    _username = HttpContext.Current.User.Identity.Name;
                    return true;
                }

                return false;
            }
        }
        #endregion

        #region Initializartion
        public override void Initialize(string name, NameValueCollection config)
        {
            if (config == null)
                throw new ArgumentNullException("config");

            // get provider name
            if (String.IsNullOrEmpty(name))
                name = "MetaDataSiteMapProvider";

            // get custom mappings
            if (!string.IsNullOrEmpty(config[SITE_MAP_FILE]))
                _siteMapFile = config[SITE_MAP_FILE];
            else
                _siteMapFile = DEFAULT_SITE_MAP_FILE;

            // get allow anonymous access
            if (!string.IsNullOrEmpty(config[ALLOW_ANONYMOUS_ACCESS]))
                _allowAnonymousAccess = Convert.ToBoolean(config[ALLOW_ANONYMOUS_ACCESS]);

            // get description
            if (string.IsNullOrEmpty(config[DESCRIPTION]))
            {
                config.Remove(DESCRIPTION);
                config.Add(DESCRIPTION, "MetaData Site Map Provider for Dynamic Data");
            }

            base.Initialize(name, config);
        }
        #endregion

        #region implemented methods
        public SiteMapNode BuildSiteMap()
        {
            if (!IsValidUser)
                return null;

            // need to make sure we start with a clean slate
            if (_rootNode.Keys.Contains(_username) && _rootNode[_username] != null)
            {
                return _rootNode[_username];
            }

            lock (_lock)
            {
                // setup user storage
                _rootNode.Add(_username, new SiteMapNode(this, "RootMetaDataSiteMapProvider"));
                _parentNodeTable.Add(_username, new Hashtable());
                _childNodeCollectionTable.Add(_username, new Hashtable());
                _urlTable.Add(_username, new Hashtable());
                _keyTable.Add(_username, new Hashtable());

                // get tables for root elements
                var tables = from t in Model.Tables
                             where t.GetAttributeOrDefault<MenuAttribute>().RootMenu
                             // sort first by menu order then by name.
                             orderby t.GetAttributeOrDefault<MenuAttribute>(), t.DisplayName
                             select t;

                // get content from site map file.
                var sitemapFile = HttpContext.Current.Server.MapPath("~/" + _siteMapFile);

                // no site map file just load meta model entries.
                foreach (var table in tables)
                {
                    SetChildren(table, _rootNode[_username], true);
                }
            }

            // not sure if this is needed no real explanation
            // was given in the samples I've seen.
            HttpRuntime.Cache.Insert(CACHE_DEPENDENCY_NAME, new object());

            return _rootNode[_username];
        }

        private void SetChildren(MetaTable table, SiteMapNode parentNode, Boolean rootMenu)
        {
            String imageUrl = String.Empty;
            var description = table.GetAttribute<DescriptionAttribute>();
            var menuAttribute = table.GetAttribute<MenuAttribute>();
            if (menuAttribute != null)
                imageUrl = menuAttribute.ImagePath;

            // get extra attributes, I'm just going to 
            // use the ImageUrl for use in menus etc.
            NameValueCollection attributes = null;
            if (!String.IsNullOrEmpty(imageUrl))
            {
                attributes = new NameValueCollection();
                attributes.Add("ImageUrl", imageUrl);
            }

            // get the title
            var menuTitle = rootMenu && menuAttribute.Name == menuAttribute.Parent
                ? menuAttribute.Name : table.DisplayName;

            // get the children nodes of this node.
            var childTables = from t in Model.Tables
                              where
                                  !String.IsNullOrEmpty(t.GetAttributeOrDefault<MenuAttribute>().Parent) &&
                                  t.GetAttributeOrDefault<MenuAttribute>().Parent == menuTitle &&
                                  t.GetAttributeOrDefault<MenuAttribute>().Show
                              orderby (!t.GetAttributeOrDefault<MenuAttribute>().RootMenu).ToString(), t.GetAttributeOrDefault<MenuAttribute>(), t.DisplayName
                              select t;

            // get the description if attribute has 
            // been applied or DisplayName if not.
            String menuDescription;
            if (description != null)
                menuDescription = description.Description;
            else if (rootMenu && menuAttribute.Name == menuAttribute.Parent && childTables.Count() > 1)
                menuDescription = menuAttribute.Name;
            else
                menuDescription = table.DisplayName;

            String key = table.ListActionPath;
            if (rootMenu && menuAttribute.Name == menuAttribute.Parent && childTables.Count() > 1)
                key = String.Format("rootItem:{0}", menuAttribute.Name);

            // get tables roles
            var usersRolesForTable = new List<String>();
            var tablePermissions = table.Attributes.OfType<SecureTableAttribute>();
            foreach (var tp in tablePermissions)
            {
                usersRolesForTable.AddRange(tp.Roles);
            }

            // note resource keys are not used I'm 
            // not bothering with localization.
            var node = new SiteMapNode(
                this,
                // note Key and URL must match
                // for selected menus to show up
                key,
                null,
                menuTitle,
                "", //menuDescription,
                usersRolesForTable,
                attributes,
                null,
                "");

            if (!rootMenu || menuAttribute.Name != menuAttribute.Parent || childTables.Count() == 1)
                node.Url = key;

            // we can't add two nodes with same URL an 
            // InvalidOperationException with be thrown if we do
            if (!String.IsNullOrEmpty(key))
                AddNode(node, parentNode);

            if (rootMenu && menuAttribute.Name == menuAttribute.Parent && childTables.Count() > 1)
            {
                // add children of current node
                foreach (var childTable in childTables)
                {
                    // call this method recursively.
                    SetChildren(childTable, node, false);
                }
            }
            if (String.IsNullOrEmpty(node.Url) && !node.HasChildNodes)
                RemoveNode(node);
        }

        protected void Clear()
        {
            if (!IsValidUser)
                return;

            try
            {
                Monitor.Enter(_lock);
                if (this._childNodeCollectionTable != null)
                    this._childNodeCollectionTable.Clear();

                if (this._urlTable != null)
                    this._urlTable.Clear();

                if (this._parentNodeTable != null)
                    this._parentNodeTable.Clear();

                if (this._keyTable != null)
                    this._keyTable.Clear();
            }
            finally
            {
                Monitor.Exit(_lock);
            }
        }

        protected override void AddNode(SiteMapNode node, SiteMapNode parentNode)
        {
            AddNode(node, parentNode, false);
        }

        protected void AddNode(SiteMapNode node, SiteMapNode parentNode, Boolean staticNode)
        {
            if (node == null)
                throw new ArgumentNullException("node");

            try
            {
                Monitor.Enter(_lock);

                bool flag = false;
                string url = node.Url;
                if (!string.IsNullOrEmpty(url))
                {
                    //System.Web.Util
                    if (VirtualPathUtility.IsAbsolute(url))
                        url = VirtualPathUtility.ToAppRelative(url);

                    var cleanedUrl = url.TruncateAtLast('/');
                    if (!staticNode)
                        url = cleanedUrl;

                    //var urlParts = url.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
                    //if (!staticNode)
                    //    url = urlParts[1];

                    //if (!staticNode && urlParts.Length > 1)
                    //    url = urlParts[urlParts.Length - 1].Replace(" ", "");

                    if (UrlTable != null && UrlTable[url] != null)
                        throw new InvalidOperationException("Multiple Nodes With Identical URL");

                    flag = true;
                }

                string key = node.Key;
                if (KeyTable.Contains(key))
                    throw new InvalidOperationException("Multiple Nodes With Identical Key");

                KeyTable[key] = node;
                if (flag)
                    UrlTable[url] = node;

                if (parentNode != null)
                {
                    ParentNodeTable[node] = parentNode;

                    if (ChildNodeCollectionTable[parentNode] == null)
                        ChildNodeCollectionTable[parentNode] = new SiteMapNodeCollection();

                    ((SiteMapNodeCollection)ChildNodeCollectionTable[parentNode]).Add(node);
                }
            }
            finally
            {
                Monitor.Exit(_lock);
            }
        }

        public override SiteMapNode FindSiteMapNode(string rawUrl)
        {
            if (!IsValidUser)
                return null;

            if (rawUrl == null)
                throw new ArgumentNullException("rawUrl");

            rawUrl = rawUrl.Trim();
            if (rawUrl.Length == 0)
                return null;

            if (VirtualPathUtility.IsAbsolute(rawUrl))
                rawUrl = VirtualPathUtility.ToAppRelative(rawUrl);

            String url = rawUrl;
            var splitUrl = rawUrl.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
            if (splitUrl.Length > 1)
                url = splitUrl[1];

            Object node = this.UrlTable[url];

            if (node == null)
                node = this.UrlTable[rawUrl];

            this.BuildSiteMap();

            return ReturnNodeIfAccessible((SiteMapNode)node);
        }

        public override SiteMapNode FindSiteMapNodeFromKey(string key)
        {
            if (!IsValidUser)
                return null;

            SiteMapNode node = base.FindSiteMapNodeFromKey(key);
            if (node == null)
                node = (SiteMapNode)this.KeyTable[key];

            return ReturnNodeIfAccessible(node);
        }

        public override SiteMapNodeCollection GetChildNodes(SiteMapNode node)
        {
            if (!IsValidUser)
                return null;
            //if (!HttpContext.Current.User.Identity.IsAuthenticated)
            //    return null;

            if (node == null)
                throw new ArgumentNullException("node");

            this.BuildSiteMap();
            SiteMapNodeCollection collection = (SiteMapNodeCollection)this.ChildNodeCollectionTable[node];
            if (collection == null)
            {
                SiteMapNode node2 = (SiteMapNode)this.KeyTable[node.Key];
                if (node2 != null)
                    collection = (SiteMapNodeCollection)this.ChildNodeCollectionTable[node2];
            }
            if (collection == null)
                return new SiteMapNodeCollection();

            if (!base.SecurityTrimmingEnabled)
                return SiteMapNodeCollection.ReadOnly(collection);

            HttpContext current = HttpContext.Current;
            SiteMapNodeCollection nodes2 = new SiteMapNodeCollection(collection.Count);
            foreach (SiteMapNode node3 in collection)
            {
                if (node3.IsAccessibleToUser(current))
                    nodes2.Add(node3);
            }
            return SiteMapNodeCollection.ReadOnly(nodes2);
        }

        public override SiteMapNode GetParentNode(SiteMapNode node)
        {
            if (!IsValidUser)
                return null;
            //if (!HttpContext.Current.User.Identity.IsAuthenticated)
            //    return null;

            if (node == null)
                throw new ArgumentNullException("node");

            this.BuildSiteMap();

            SiteMapNode parentNode = (SiteMapNode)this.ParentNodeTable[node];
            if (parentNode == null)
            {
                SiteMapNode node3 = (SiteMapNode)this.KeyTable[node.Key];
                if (node3 != null)
                    parentNode = (SiteMapNode)this.ParentNodeTable[node3];
            }

            if ((parentNode == null) && (this.ParentProvider != null))
                parentNode = this.ParentProvider.GetParentNode(node);

            return ReturnNodeIfAccessible(parentNode);
        }

        protected override void RemoveNode(SiteMapNode node)
        {
            if (node == null)
                throw new ArgumentNullException("node");

            try
            {
                Monitor.Enter(_lock);
                SiteMapNode node2 = (SiteMapNode)this.ParentNodeTable[node];
                if (this.ParentNodeTable.Contains(node))
                    this.ParentNodeTable.Remove(node);

                if (node2 != null)
                {
                    SiteMapNodeCollection nodes = (SiteMapNodeCollection)this.ChildNodeCollectionTable[node2];
                    if ((nodes != null) && nodes.Contains(node))
                        nodes.Remove(node);
                }
                string url = node.Url;
                if (((url != null) && (url.Length > 0)) && this.UrlTable.Contains(url))
                    this.UrlTable.Remove(url);

                string key = node.Key;
                if (this.KeyTable.Contains(key))
                    this.KeyTable.Remove(key);
            }
            finally
            {
                Monitor.Exit(_lock);
            }
        }

        protected override SiteMapNode GetRootNodeCore()
        {
            if (!IsValidUser)
                return null;

            BuildSiteMap();
            return _rootNode[_username];
        }

        internal SiteMapNode ReturnNodeIfAccessible(SiteMapNode node)
        {
            if ((node != null) && node.IsAccessibleToUser(HttpContext.Current))
                return node;

            return null;
        }

        internal IDictionary ChildNodeCollectionTable
        {
            get
            {
                //if (!HttpContext.Current.User.Identity.IsAuthenticated)
                //    return null;

                if (this._childNodeCollectionTable[_username] == null)
                {
                    try
                    {
                        Monitor.Enter(_lock);
                        if (this._childNodeCollectionTable[_username] == null)
                            this._childNodeCollectionTable[_username] = new Hashtable();
                    }
                    finally
                    {
                        Monitor.Exit(_lock);
                    }
                }
                return this._childNodeCollectionTable[_username];
            }
        }

        internal IDictionary KeyTable
        {
            get
            {
                //if (!HttpContext.Current.User.Identity.IsAuthenticated)
                //    return null;

                if (this._keyTable[_username] == null)
                {
                    try
                    {
                        Monitor.Enter(_lock);
                        if (this._keyTable[_username] == null)
                            this._keyTable[_username] = new Hashtable();
                    }
                    finally
                    {
                        Monitor.Exit(_lock);
                    }
                }
                return this._keyTable[_username];
            }
        }

        internal IDictionary ParentNodeTable
        {
            get
            {
                //if (!HttpContext.Current.User.Identity.IsAuthenticated)
                //    return null;

                if (this._parentNodeTable == null)
                {
                    try
                    {
                        Monitor.Enter(_lock);
                        if (this._parentNodeTable[_username] == null)
                            this._parentNodeTable[_username] = new Hashtable();
                    }
                    finally
                    {
                        Monitor.Exit(_lock);
                    }
                }
                return this._parentNodeTable[_username];
            }
        }

        internal IDictionary UrlTable
        {
            get
            {
                //if (!HttpContext.Current.User.Identity.IsAuthenticated)
                //    return null;

                if (this._urlTable[_username] == null)
                {
                    try
                    {
                        Monitor.Enter(_lock);
                        if (this._urlTable[_username] == null)
                            this._urlTable[_username] = new Hashtable(StringComparer.OrdinalIgnoreCase);
                    }
                    finally
                    {
                        Monitor.Exit(_lock);
                    }
                }
                return this._urlTable[_username];
            }
        }
        #endregion

        //public override bool IsAccessibleToUser(HttpContext context, SiteMapNode node)
        //{
        //    if (node == null)
        //    {
        //        throw new ArgumentNullException("node");
        //    }
        //    if (context == null)
        //    {
        //        throw new ArgumentNullException("context");
        //    }
        //    if (!this.SecurityTrimmingEnabled)
        //    {
        //        return true;
        //    }
        //    if (node.Roles != null)
        //    {
        //        foreach (string str in node.Roles)
        //        {
        //            if ((str == "*") || ((context.User != null) && context.User.IsInRole(str)))
        //            {
        //                return true;
        //            }
        //        }
        //    }
        //    System.Web.VirtualPath virtualPath = node.VirtualPath;
        //    return (((virtualPath != null) && virtualPath.IsWithinAppRoot) && Util.IsUserAllowedToPath(context, virtualPath));
        //}
    }
}